热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

媒体文件|都会_最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)

篇首语:本文由编程笔记#小编为大家整理,主要介绍了最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)相关的知识,希望对你有一定的参考价值。最简单的基于FFM

篇首语:本文由编程笔记#小编为大家整理,主要介绍了最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)相关的知识,希望对你有一定的参考价值。



最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)

最近项目需要,开始学习ffmpeg。网上资料很多,但是大多数资料都是几年前的。ffmpeg 的API 这几年变化蛮大的。按照网上的教程来写代码会遇到各种问题。所以我才想写这么一个专栏。用ffmpeg 比较新的 API 来把一些常用的功能都写一遍。

我大概学习 ffmpeg 有一周多了。感觉学习 ffmpeg 有三个资源可以好好利用。


  1. 机械工业出版社出版, 刘歧、赵文杰编著的《FFmpeg从入门到精通》
  2. 雷霄骅 的博客 https://blog.csdn.net/leixiaohua1020
  3. ffmpeg 的源代码,尤其是 doc\\examples 目录下的例子程序。

我的这篇博客其实就是雷神的一篇博客(https://blog.csdn.net/leixiaohua1020/article/details/25422685)的更新版。

雷霄骅,中国传媒大学通信与信息系统专业博士生,写了很多音视频编解码方面的科普文章,被人们尊称为雷神。可惜天妒英才,2016 年猝死于实验室。他的博客确实写得很好,值得每一位准备学习音视频编解码的同学好好的读读。他的博客里也给出了很多 ffmpeg 的例子程序,但是由于ffmpeg API 版本更新,他的程序代码很多已经无法编译运行了。我准备写一系列的博客,把他的代码做少许修改,让这些代码能在最新版的 ffmpeg 上运行。

关于这篇博客中需要用到的 ffmpeg 的基础知识,请读雷神的博客 https://blog.csdn.net/leixiaohua1020/article/details/25422685。

首先,在正式程序开始之前。我先给一个更简单的例子程序。这个程序会打开一个媒体文件,然后输出这个媒体文件的一些基本信息。这里我们的媒体文件来自 https://samples.mplayerhq.hu/avi/AV36_1.avi 。

这里来介绍一下 https://samples.mplayerhq.hu 这个网站。大名鼎鼎的 mplayer 想必大家都听说过, samples.mplayerhq.hu 站点上的文件就是 mplayer 提供的用来测试 mplayer 解码能力的测试样本。基本上你听说过的、没听说过的格式文件都有。并且每个文件都不太大,用来测试我们的程序最合适不过。

下面是例子代码:

extern "C"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
void msg(const char * str, int ret)
static char err[512];
if(ret <0)

av_strerror(ret, err, 1024);
printf("%s error: %s\\n", str, err);
exit(ret);

else

printf("%s : success.\\n", str);

void test00()
const char * filename &#61; "D:\\\\AV36_1.avi";
AVFormatContext *pFormatCtx &#61; avformat_alloc_context();
msg("avformat_open_input", avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));
msg("avformat_find_stream_info", avformat_find_stream_info(pFormatCtx, nullptr));
av_dump_format(pFormatCtx, 0, filename, 0);
avformat_close_input(&pFormatCtx);
avformat_free_context(pFormatCtx);

这个代码的输出结果如下&#xff1a;

avformat_open_input : success.
avformat_find_stream_info : success.
Input #0, avi, from &#39;D:\\AV36_1.avi&#39;:
Duration: 00:00:32.93, start: 0.000000, bitrate: 2372 kb/s
Stream #0:0: Video: indeo5 (IV50 / 0x30355649), yuv410p, 320x240, 2058 kb/s, 15 fps, 15 tbr, 15 tbn, 15 tbc
Metadata:
title : Steyr.avi &#xfffd;&#xfffd;&#xfffd;#1
Stream #0:1: Audio: adpcm_ms ([2][0][0][0] / 0x0002), 22050 Hz, 2 channels, s16, 176 kb/s
Metadata:
title : Sound Forge 4.0 Audio

这里简单解释几点&#xff1a;


  1. msg() 函数是个辅助函数。用这个函数就不用像雷神的代码那样每一个函数调用都要用if 语句去判断返回值了。如果调用失败&#xff0c;会直接给出错误提示并退出程序。

  2. 另外就是这个代码其实不需要调用 avformat_find_stream_info()。因为 av_dump_format()不要求提前调用avformat_find_stream_info() 的。不过我们后面的封装格式转换程序需要这个操作&#xff0c;所以这里也没去掉这一步。

  3. avformat_alloc_context()和avformat_free_context() 配对的&#xff0c; avformat_open_input() 和avformat_close_input() 是配对的。

  4. AVFormatContext 是什么&#xff1f;简单的说一个媒体文件就要对应一个 AVFormatContext。所谓的Context 其实就是指的这个媒体文件的内容。

另外&#xff0c;我们可以看到这个视频文件中有两个 Steam。第一个 Stream 是视频流&#xff0c;编码格式是 indeo5。第二个Stream 是音频流&#xff0c;编码格式是 adpcm_ms。

好了下面开始我们的工作。我们要把AV36_1.avi 转换为 AV36_1.mkv 文件。并且保持 音视频的编码格式不变。

代码如下&#xff1a;

void test00()
const char * filename &#61; "D:\\\\AV36_1.avi";
const char * filename2 &#61; "D:\\\\AV36_1.mkv";
AVFormatContext *pFormatCtx &#61; avformat_alloc_context();
msg("avformat_open_input",
avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));
msg("avformat_find_stream_info",
avformat_find_stream_info(pFormatCtx, nullptr));
AVFormatContext *pFormatCtx2 &#61; avformat_alloc_context();
msg("avformat_alloc_output_context2",
avformat_alloc_output_context2(&pFormatCtx2, nullptr, nullptr, filename2));
for (int i &#61; 0; i nb_streams; i&#43;&#43;)

AVStream *in_stream &#61; pFormatCtx->streams[i];
AVStream * out_stream &#61; avformat_new_stream(pFormatCtx2, NULL);
avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
out_stream->codecpar->codec_tag &#61; 0;

av_dump_format(pFormatCtx2, 0, filename2, 1);
if (!(pFormatCtx2->oformat->flags & AVFMT_NOFILE))

msg("avio_open",
avio_open(&pFormatCtx2->pb, filename2, AVIO_FLAG_WRITE));

msg("avformat_write_header", avformat_write_header(pFormatCtx2, nullptr));
AVPacket pkt;
while (1)

int ret &#61; av_read_frame(pFormatCtx, &pkt);
if (ret <0) break;
AVStream * in_stream &#61; pFormatCtx->streams[pkt.stream_index];
AVStream * out_stream &#61; pFormatCtx2->streams[pkt.stream_index];
/* copy packet */
pkt.pts &#61; av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts &#61; av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration &#61; av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos &#61; -1;
av_interleaved_write_frame(pFormatCtx2, &pkt);
av_packet_unref(&pkt);

msg("av_write_trailer", av_write_trailer(pFormatCtx2));
avformat_close_input(&pFormatCtx2);
avformat_free_context(pFormatCtx2);
avformat_close_input(&pFormatCtx);
avformat_free_context(pFormatCtx);

输出结果如下&#xff0c;可以看到执行成功了&#xff1a;

avformat_open_input : success.
avformat_find_stream_info : success.
avformat_alloc_output_context2 : success.
Output #0, matroska, to &#39;D:\\AV36_1.mkv&#39;:
Stream #0:0: Video: indeo5, yuv410p, 320x240, q&#61;2-31, 2058 kb/s
Stream #0:1: Audio: adpcm_ms, 22050 Hz, 2 channels, s16, 176 kb/s
avio_open : success.
avformat_write_header : success.
av_write_trailer : success.

下面主要来讲讲和雷神的代码有区别的几行。在雷神的代码中&#xff0c;复制 Stream 时用到如下的代码块&#xff1a;

ret &#61; avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret <0)
printf( "Failed to copy context from input to output stream codec context\\n");
goto end;

在新版的 ffmpeg 中&#xff0c;avcodec_copy_context() 函数已经被废弃。我们不应该直接复制 AVStream 中的 codec。而应该复制 codecpar&#xff0c;所以需要改用 avcodec_parameters_copy() 函数。

out_stream->codec->codec_tag &#61; 0;
也需要更新为&#xff1a;
out_stream->codecpar->codec_tag &#61; 0;

雷神还有几行代码&#xff1a;

if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |&#61; CODEC_FLAG_GLOBAL_HEADER;

这几行代码中的CODEC_FLAG_GLOBAL_HEADER 也已经不存在了&#xff0c;可以改为 AV_CODEC_FLAG_GLOBAL_HEADER。或者向我这样干脆就不要了。

剩下的代码变化不大&#xff0c;我觉得值得讲一讲的是下面这几行代码&#xff1a;

pkt.pts &#61; av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts &#61; av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration &#61; av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);

这三行代码的作用是调整 pts&#xff0c;dts 和 duration。最开始我一直不理解&#xff0c;我又没有转码&#xff0c;这三个时间按说不会有变化&#xff0c;为啥要调整。后来我输出了 in_stream->time_base 和 out_stream->time_base 的值&#xff0c;才发现这两个值并不相同。按照我现在的理解&#xff0c;time_base 是和封装格式相关的。 avi 文件的 time_base 和 mkv 文件的 time_base 不同&#xff0c;而pts、dts 和 duration 都是以 time_base 作为基本单位的 &#xff0c;所以需要转换一下。

这里还想再吐槽一下&#xff0c;av_read_frame() 输出的结果根本不是 frame &#xff0c;而是 AVPacket。frame 和 packet 有什么关系呢&#xff1f;简单的说一个 packet 里面可能会有若干个 frame。具体有几个需要用解码器解一下才能确定。一个 packet 里的数据是属于某一个 stream 的&#xff0c;不会出现一个 packet 里既有视频数据又有音频数据的。具体属于哪个 stream 要通过 pkt.stream_index 来确定。那么如果我们只想要视频的 packet 怎么办呢&#xff1f;没有什么简便的办法&#xff0c;只能先获得一个packet 然后判读是不是我们要的那个 stream 的。如果不是就 av_packet_unref() &#xff0c;然后再读。

有些封装格式除了有视频流、音频流之外&#xff0c;还会有字幕流等其他的流。下面给个例子代码&#xff0c;只选择出视频流、音频流&#xff0c;其他的流都忽略。这个代码基本上是来自 doc\\examples\\remuxing.c , 我只做了少许的改写。

const char * filename &#61; "D:\\\\AV36_1.avi";
const char * filename2 &#61; "D:\\\\AV36_1.mkv";
qDebug() <<"hello";
AVFormatContext *pFormatCtx &#61; avformat_alloc_context();
msg("avformat_open_input",
avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));
msg("avformat_find_stream_info",
avformat_find_stream_info(pFormatCtx, nullptr));
av_dump_format(pFormatCtx, 0, filename, 0);
int stream_index &#61; 0;
int *stream_mapping &#61; NULL;
int stream_mapping_size &#61; 0;
stream_mapping_size &#61; pFormatCtx->nb_streams;
stream_mapping &#61; (int *)av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
AVFormatContext *pFormatCtx2 &#61; avformat_alloc_context();
msg("avformat_alloc_output_context2",
avformat_alloc_output_context2(&pFormatCtx2, nullptr, nullptr, filename2));
qDebug() <<"pFormatCtx->nb_streams &#61; " <nb_streams;
for (int i &#61; 0; i nb_streams; i&#43;&#43;)

AVStream *in_stream &#61; pFormatCtx->streams[i];
AVCodecParameters *in_codecpar &#61; in_stream->codecpar;
qDebug() <<"streams[" <codec_type;
if (in_codecpar->codec_type !&#61; AVMEDIA_TYPE_AUDIO &&// AVMEDIA_TYPE_AUDIO &&
in_codecpar->codec_type !&#61; AVMEDIA_TYPE_VIDEO &&
in_codecpar->codec_type !&#61; AVMEDIA_TYPE_SUBTITLE)

stream_mapping[i] &#61; -1;
continue;

stream_mapping[i] &#61; stream_index&#43;&#43;;
AVStream * out_stream &#61; avformat_new_stream(pFormatCtx2, NULL);
avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
out_stream->codecpar->codec_tag &#61; 0;

av_dump_format(pFormatCtx2, 0, filename2, 1);
if (!(pFormatCtx2->oformat->flags & AVFMT_NOFILE))

msg("avio_open",
avio_open(&pFormatCtx2->pb, filename2, AVIO_FLAG_WRITE));

msg("avformat_write_header", avformat_write_header(pFormatCtx2, nullptr));
AVPacket pkt;
while (1)

int ret &#61; av_read_frame(pFormatCtx, &pkt);
if (ret <0) break;
if (pkt.stream_index >&#61; stream_mapping_size || stream_mapping[pkt.stream_index] <0)

av_packet_unref(&pkt);
continue;

pkt.stream_index &#61; stream_mapping[pkt.stream_index];
AVStream * in_stream &#61; pFormatCtx->streams[pkt.stream_index];
AVStream * out_stream &#61; pFormatCtx2->streams[pkt.stream_index];
/* copy packet */
pkt.pts &#61; av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts &#61; av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration &#61; av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos &#61; -1;
av_interleaved_write_frame(pFormatCtx2, &pkt);
av_packet_unref(&pkt);

msg("av_write_trailer", av_write_trailer(pFormatCtx2));
avformat_close_input(&pFormatCtx);
/* close output */
if (pFormatCtx2 && !(pFormatCtx2->flags & AVFMT_NOFILE))

avio_closep(&pFormatCtx2->pb);

avformat_free_context(pFormatCtx2);
av_freep(&stream_mapping);

至此&#xff0c;本篇博客的主要内容就都讲完了。这里多说几句题外话。


  1. 在 win 下编译 ffmpeg 可以用 vcpkg&#xff0c;很方便。当然用 msys2 环境去编译也不难。
  2. 多读 doc\\examples 下的代码。网上找的代码片段可能会失效&#xff0c;doc\\examples 的代码永远不会。ffmpeg 的作者们在发布新版 ffmpeg 前&#xff0c;都会保证 doc\\examples 的代码是可编译运行的。
  3. doc\\examples 的代码很多&#xff0c;怎么入门呢。从简单的开始入手。哪个文件小就从哪个文件开始学。或者读雷霄骅的博客&#xff0c;然后在 doc\\examples里面找对应的代码。

这篇博客的代码用C&#43;&#43; 改写了一遍&#xff1a;
https://blog.csdn.net/liyuanbhu/article/details/121744275


推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 成功安装Sabayon Linux在thinkpad X60上的经验分享
    本文分享了作者在国庆期间在thinkpad X60上成功安装Sabayon Linux的经验。通过修改CHOST和执行emerge命令,作者顺利完成了安装过程。Sabayon Linux是一个基于Gentoo Linux的发行版,可以将电脑快速转变为一个功能强大的系统。除了作为一个live DVD使用外,Sabayon Linux还可以被安装在硬盘上,方便用户使用。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
author-avatar
金爽20111018
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有